/**
 * Codex CLI Service - Node implementation inspired by original Python adapter
 */

import { spawn } from 'child_process';
import readline from 'node:readline';
import path from 'path';
import fs from 'fs/promises';
import { randomUUID } from 'crypto';
import type { Message } from '@/types/backend';
import type { RealtimeMessage } from '@/types';
import { streamManager } from '@/lib/services/stream';
import { createMessage } from '@/lib/services/message';
import { getProjectById } from '@/lib/services/project';
import { getDefaultModelForCli } from '@/lib/constants/cliModels';
import { CODEX_DEFAULT_MODEL, getCodexModelDisplayName, normalizeCodexModelId } from '@/lib/constants/codexModels';
import {
  markUserRequestAsRunning,
  markUserRequestAsCompleted,
  markUserRequestAsFailed,
} from '@/lib/services/user-requests';
import { serializeMessage, createRealtimeMessage } from '@/lib/serializers/chat';

type ToolAction = 'Write' | 'Edit' | 'Delete' | 'Bash' | 'Info';

interface CodexEvent {
  id?: string;
  msg?: {
    type?: string;
    delta?: string;
    message?: string;
    command?: string[] | string;
    summary?: string;
    changes?: Record<string, unknown> | Array<Record<string, unknown>>;
    [key: string]: any;
  };
}

const AUTO_INSTRUCTIONS = `Act autonomously without asking for confirmations.
Use apply_patch to create and modify files directly in the current working directory (do not create subdirectories unless the user explicitly requests it).
Use exec_command to run, build, and test as needed.
You have full permissions. Keep taking concrete actions until the task is complete.
Respect the existing project structure (Next.js App Router with TypeScript and Tailwind) when creating or modifying files.
In projects generated by create-next-app, the main UI lives in app/page.tsx and related files under app/. Prioritize edits there unless the user specifies otherwise.
Prefer concise status updates over questions.`;

const STATUS_LABELS: Record<string, string> = {
  starting: 'Initializing Codex CLI...',
  ready: 'Codex session ready',
  running: 'Codex is processing the request...',
  completed: 'Codex execution completed',
};

const CODEX_ENV = () => {
  const env = { ...process.env };
  const additionalPaths: string[] = [];
  const npmGlobal = process.env.NPM_GLOBAL_PATH;
  if (npmGlobal) {
    additionalPaths.push(npmGlobal);
  }
  if (process.platform === 'win32') {
    const appData = process.env.APPDATA;
    const localApp = process.env.LOCALAPPDATA;
    if (appData) {
      additionalPaths.push(path.join(appData, 'npm'));
    }
    if (localApp) {
      additionalPaths.push(path.join(localApp, 'Programs', 'nodejs'));
    }
  }
  if (additionalPaths.length > 0) {
    const existing = env.PATH || env.Path || '';
    env.PATH = [...additionalPaths, existing].filter(Boolean).join(path.delimiter);
  }
  return env;
};

const CODEX_EXECUTABLE = process.platform === 'win32' ? 'codex.cmd' : 'codex';

function publishStatus(projectId: string, status: string, requestId?: string, message?: string) {
  streamManager.publish(projectId, {
    type: 'status',
    data: {
      status,
      message: message ?? STATUS_LABELS[status] ?? '',
      ...(requestId ? { requestId } : {}),
    },
  });
}

async function ensureProjectPath(projectId: string, projectPath: string): Promise<string> {
  const project = await getProjectById(projectId);
  if (!project) {
    throw new Error(`Project not found: ${projectId}`);
  }

  const absolute = path.isAbsolute(projectPath)
    ? path.resolve(projectPath)
    : path.resolve(process.cwd(), projectPath);
  const allowedBasePath = path.resolve(process.cwd(), process.env.PROJECTS_DIR || './data/projects');
  const relativeToBase = path.relative(allowedBasePath, absolute);
  const isWithinBase = !relativeToBase.startsWith('..') && !path.isAbsolute(relativeToBase);
  if (!isWithinBase) {
    throw new Error(`Project path must be within ${allowedBasePath}. Got: ${absolute}`);
  }

  try {
    await fs.access(absolute);
  } catch {
    await fs.mkdir(absolute, { recursive: true });
  }

  return absolute;
}

function summarizeApplyPatch(payload: CodexEvent['msg']): { content: string; metadata: Record<string, unknown> } {
  const changes = payload?.changes;
  const files: string[] = [];
  if (Array.isArray(changes)) {
    for (const entry of changes) {
      const file = typeof entry === 'object' ? (entry?.path || entry?.file) : undefined;
      if (file && typeof file === 'string') {
        files.push(file);
      }
    }
  } else if (changes && typeof changes === 'object') {
    for (const key of Object.keys(changes)) {
      files.push(key);
    }
  }

  const unique = Array.from(new Set(files));
  const summary =
    unique.length === 0
      ? 'Applied file changes'
      : unique.length === 1
      ? `Updated ${unique[0]}`
      : `Updated ${unique.length} files (${unique.slice(0, 3).join(', ')}${unique.length > 3 ? ', ...' : ''})`;

  return {
    content: summary,
    metadata: {
      cli_type: 'codex',
      tool_name: 'Edit',
      files: unique,
    },
  };
}

function summarizeExecCommand(payload: CodexEvent['msg']): { content: string; metadata: Record<string, unknown> } {
  const command = Array.isArray(payload?.command)
    ? payload?.command.join(' ')
    : typeof payload?.command === 'string'
    ? payload.command
    : '';
  const summary = command ? `Ran: ${command}` : 'Executed shell command';

  return {
    content: summary,
    metadata: {
      cli_type: 'codex',
      tool_name: 'Bash',
      command,
    },
  };
}

function summarizeWebSearch(payload: CodexEvent['msg']): { content: string; metadata: Record<string, unknown> } {
  const query = typeof payload?.query === 'string' ? payload.query : '';
  const summary = query ? `Searching: ${query}` : 'Running web search';
  return {
    content: summary,
    metadata: {
      cli_type: 'codex',
      tool_name: 'WebSearch',
      query: query || undefined,
    },
  };
}

function summarizeMcpTool(payload: CodexEvent['msg']): { content: string; metadata: Record<string, unknown> } {
  const invocation = (payload?.invocation as Record<string, unknown>) ?? {};
  const server = typeof invocation.server === 'string' ? invocation.server : undefined;
  const tool = typeof invocation.tool === 'string' ? invocation.tool : undefined;
  const parts: string[] = [];
  if (tool) parts.push(tool);
  if (server) parts.push(`server: ${server}`);
  const summary = parts.length > 0 ? `Using MCP tool (${parts.join(', ')})` : 'Using MCP tool';
  return {
    content: summary,
    metadata: {
      cli_type: 'codex',
      tool_name: 'MCPTool',
      server,
      tool,
    },
  };
}

type TodoListPhase = 'started' | 'update' | 'completed';

interface TodoListItem {
  text: string;
  completed: boolean;
  index: number;
}

function extractTodoListItems(record: Record<string, unknown>): unknown {
  if (Array.isArray(record.items)) {
    return record.items;
  }
  const nestedItem = record.item;
  if (nestedItem && typeof nestedItem === 'object' && Array.isArray((nestedItem as Record<string, unknown>).items)) {
    return (nestedItem as Record<string, unknown>).items;
  }
  const delta = record.delta;
  if (delta && typeof delta === 'object' && Array.isArray((delta as Record<string, unknown>).items)) {
    return (delta as Record<string, unknown>).items;
  }
  return [];
}

function normalizeTodoListItems(input: unknown): TodoListItem[] {
  if (!Array.isArray(input)) {
    return [];
  }

  const result: TodoListItem[] = [];

  input.forEach((entry, index) => {
    if (!entry || typeof entry !== 'object') {
      return;
    }
    const record = entry as Record<string, unknown>;
    const text = pickFirstString(record.text) ?? `Step ${index + 1}`;
    const completed = record.completed === true || record.done === true;
    result.push({
      text,
      completed,
      index,
    });
  });

  return result;
}

function buildTodoListContent(items: TodoListItem[], phase: TodoListPhase): string {
  if (items.length === 0) {
    switch (phase) {
      case 'started':
        return 'Started plan with no explicit steps.';
      case 'completed':
        return 'Plan completed.';
      default:
        return 'Plan updated.';
    }
  }

  const header =
    phase === 'completed'
      ? 'Plan completed:'
      : phase === 'started'
      ? 'Plan generated:'
      : 'Plan updated:';

  const stepLines = items.map((item, idx) => {
    const bullet = item.completed ? '✅' : '⬜️';
    const label = `Step ${idx + 1}`;
    return `${bullet} ${label}: ${item.text}`;
  });

  return [header, ...stepLines].join('\n');
}

function createTodoListMetadata(items: TodoListItem[], phase: TodoListPhase, extra?: Record<string, unknown>) {
  const totalSteps = items.length;
  const completedSteps = items.filter((item) => item.completed).length;
  return {
    toolName: 'Plan',
    tool_name: 'Plan',
    planPhase: phase,
    planStatus: phase === 'completed' ? 'completed' : 'in_progress',
    totalSteps,
    completedSteps,
    items: items.map(({ text, completed, index }) => ({
      text,
      completed,
      index,
    })),
    ...(extra ?? {}),
  };
}

async function persistMessage(
  projectId: string,
  payload: {
    id?: string;
    role: Message['role'];
    messageType: Message['messageType'];
    content: string;
    metadata?: Record<string, unknown> | null;
  },
  requestId?: string,
  overrides?: Partial<RealtimeMessage>
) {
  try {
    const saved = await createMessage({
      ...(payload.id ? { id: payload.id } : {}),
      projectId,
      role: payload.role,
      messageType: payload.messageType,
      content: payload.content,
      metadata: payload.metadata ?? null,
      cliSource: 'codex',
      requestId,
    });

    const serializedOverrides = {
      ...(requestId ? { requestId } : {}),
      ...(overrides ?? {}),
    };

    streamManager.publish(projectId, {
      type: 'message',
      data: serializeMessage(saved, serializedOverrides),
    });
  } catch (error) {
    console.error('[CodexService] Failed to persist message. Falling back to realtime emit:', error);
    const fallback = createRealtimeMessage({
      id: payload.id ?? randomUUID(),
      projectId,
      role: payload.role,
      messageType: payload.messageType,
      content: payload.content,
      metadata: payload.metadata ?? null,
      cliSource: 'codex',
      requestId,
      ...(overrides ?? {}),
    });
    streamManager.publish(projectId, {
      type: 'message',
      data: fallback,
    });
  }
}

const encodeHash = (value: string): string =>
  Buffer.from(value, 'utf-8').toString('base64');

async function dispatchToolMessage(
  projectId: string,
  content: string,
  metadata: Record<string, unknown>,
  requestId?: string,
  options: {
    messageType?: 'tool_use' | 'tool_result';
    persist?: boolean;
    isStreaming?: boolean;
    streamedToolHashes?: Set<string>;
  } = {},
) {
  const trimmedContent = content.trim();
  if (!trimmedContent) {
    return;
  }

  const { messageType = 'tool_use', persist = true, isStreaming = false, streamedToolHashes } = options;

  // Enhanced duplicate detection for tool messages
  const toolHash = encodeHash(`${messageType}:${trimmedContent}:${JSON.stringify(metadata)}`).substring(0, 16);
  if (streamedToolHashes?.has(toolHash)) {
    console.debug(`[CodexService] Tool message already processed (hash: ${toolHash}), skipping`);
    return;
  }

  const enrichedMetadata: Record<string, unknown> = {
    cli_type: 'codex',
    ...(metadata ?? {}),
  };
  const snakeToolName = typeof enrichedMetadata['tool_name'] === 'string' ? (enrichedMetadata['tool_name'] as string) : undefined;
  const camelToolName = typeof enrichedMetadata['toolName'] === 'string' ? (enrichedMetadata['toolName'] as string) : undefined;
  if (!camelToolName && snakeToolName) {
    enrichedMetadata['toolName'] = snakeToolName;
  }
  if (!snakeToolName && camelToolName) {
    enrichedMetadata['tool_name'] = camelToolName;
  }

  if (streamedToolHashes) {
    streamedToolHashes.add(toolHash);
  }

  if (!persist) {
    const transientMetadata = {
      ...enrichedMetadata,
      isTransientToolMessage: true,
    };
    const realtime = createRealtimeMessage({
      projectId,
      role: 'tool',
      messageType,
      content: trimmedContent,
      metadata: transientMetadata,
      cliSource: 'codex',
      requestId,
      isStreaming,
      isFinal: !isStreaming,
    });
    streamManager.publish(projectId, { type: 'message', data: realtime });
    console.debug(`[CodexService] Dispatched transient tool message (hash: ${toolHash})`);
    return;
  }

  await persistMessage(
    projectId,
    {
      role: 'tool',
      messageType,
      content: trimmedContent,
      metadata: enrichedMetadata,
    },
    requestId,
    { isStreaming, isFinal: !isStreaming },
  );
  console.debug(`[CodexService] Persisted tool message (hash: ${toolHash})`);
}

const pickFirstString = (value: unknown): string | undefined => {
  if (typeof value === 'string') {
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : undefined;
  }
  if (typeof value === 'number' || typeof value === 'boolean') {
    return String(value);
  }
  if (Array.isArray(value)) {
    for (const entry of value) {
      const candidate = pickFirstString(entry);
      if (candidate) {
        return candidate;
      }
    }
    return undefined;
  }
  if (value && typeof value === 'object') {
    const record = value as Record<string, unknown>;
    for (const key of Object.keys(record)) {
      const candidate = pickFirstString(record[key]);
      if (candidate) {
        return candidate;
      }
    }
  }
  return undefined;
};

async function appendProjectContext(baseInstruction: string, repoPath: string): Promise<string> {
  try {
    const entries = await fs.readdir(repoPath, { withFileTypes: true });
    const visible = entries
      .filter((entry) => !entry.name.startsWith('.git') && entry.name !== 'AGENTS.md')
      .map((entry) => entry.name);
    if (visible.length === 0) {
      return `${baseInstruction}

<current_project_context>
This is an empty project directory. Work directly in the current folder without creating extra subdirectories.
</current_project_context>`;
    }
    return `${baseInstruction}

<current_project_context>
Current files in project directory: ${visible.sort().join(', ')}
Work directly in the current directory. Do not create subdirectories unless specifically requested.
</current_project_context>`;
  } catch (error) {
    console.warn('[CodexService] Failed to append project context:', error);
    return baseInstruction;
  }
}

async function executeCodex(
  projectId: string,
  projectPath: string,
  instruction: string,
  model: string,
  requestId?: string,
  isInitialPrompt: boolean = false,
): Promise<void> {
  const normalizedModel = normalizeCodexModelId(model);
  const modelDisplayName = getCodexModelDisplayName(normalizedModel);
  publishStatus(projectId, 'starting', requestId);

  if (requestId) {
    await markUserRequestAsRunning(requestId);
  }

  const absoluteProjectPath = await ensureProjectPath(projectId, projectPath);
  const repoPath = await (async () => {
    const candidate = path.join(absoluteProjectPath, 'repo');
    try {
      const stats = await fs.stat(candidate);
      if (stats.isDirectory()) {
        return candidate;
      }
    } catch {
      // ignore
    }
    return absoluteProjectPath;
  })();

  publishStatus(projectId, 'ready', requestId, `Codex CLI detected (${modelDisplayName}). Starting execution...`);

  const promptBase = instruction.trim();
  const promptWithContext = await appendProjectContext(promptBase, repoPath);

  const codexConfigArgs = [
    '-c',
    'include_apply_patch_tool=true',
    '-c',
    'include_plan_tool=true',
    '-c',
    'tools.web_search_request=true',
    '-c',
    'use_experimental_streamable_shell_tool=true',
    '-c',
    'sandbox_mode=danger-full-access',
    '-c',
    'max_turns=20',
    '-c',
    'max_thinking_tokens=4096',
    '-c',
    `instructions=${JSON.stringify(AUTO_INSTRUCTIONS)}`,
  ];

  const codexArgs = [
    'exec',
    '--json',
    '--skip-git-repo-check',
    '--dangerously-bypass-approvals-and-sandbox',
    '--color',
    'never',
    '--cd',
    repoPath,
    ...codexConfigArgs,
    '--model',
    normalizedModel,
    promptWithContext,
  ];

  console.log('[CodexService] Spawning Codex CLI', {
    projectId,
    repoPath,
    model: normalizedModel,
    requestId,
  });

  const child = spawn(CODEX_EXECUTABLE, codexArgs, {
    cwd: repoPath,
    env: CODEX_ENV(),
    stdio: ['pipe', 'pipe', 'pipe'],
  });

  const stderrBuffer: string[] = [];
  child.stderr?.on('data', (chunk) => {
    const text = String(chunk).trim();
    if (text) {
      stderrBuffer.push(text);
      console.error('[CodexService][stderr]', text);
    }
  });

  const rl = readline.createInterface({ input: child.stdout });
  const activeCommands = new Map<string, { command?: string }>();
  const thinkingSegments: string[] = [];
  let agentBuffer = '';
  let hasCompleted = false;
  let assistantMessageId: string | null = null;
  let lastStreamedAssistantPayload: string | null = null;

  // Enhanced message tracking to prevent duplicates
  const streamedMessageIds = new Set<string>();
  const streamedToolHashes = new Set<string>();

  const buildAssistantPayload = () => {
    const trimmedAssistant = agentBuffer.trim();
    const thinkingContent = thinkingSegments
      .map((segment) => segment.trim())
      .filter((segment) => segment.length > 0)
      .map((segment) => `<thinking>${segment}</thinking>`)
      .join('\n\n');

    const parts: string[] = [];
    if (thinkingContent) {
      parts.push(thinkingContent);
    }
    if (trimmedAssistant) {
      parts.push(trimmedAssistant);
    }
    return parts.join('\n\n').trim();
  };

  const streamAssistantDraft = (force = false) => {
    const combined = buildAssistantPayload();
    if (!combined) {
      return;
    }
    if (!force && combined === lastStreamedAssistantPayload) {
      return;
    }
    const id = assistantMessageId ?? (assistantMessageId = randomUUID());

    // Enhanced duplicate prevention
    if (streamedMessageIds.has(id) && !force) {
      console.debug(`[CodexService] Assistant message ${id} already streamed, skipping`);
      return;
    }

    const realtime = createRealtimeMessage({
      id,
      projectId,
      role: 'assistant',
      messageType: 'chat',
      content: combined,
      metadata: { cli_type: 'codex' },
      cliSource: 'codex',
      requestId,
      isStreaming: true,
      isFinal: false,
    });
    streamManager.publish(projectId, { type: 'message', data: realtime });
    streamedMessageIds.add(id);
    lastStreamedAssistantPayload = combined;
    console.debug(`[CodexService] Streamed assistant message: ${id} (length: ${combined.length})`);
  };

  const resetAssistantBuffers = () => {
    agentBuffer = '';
    thinkingSegments.length = 0;
  };

  const flushAssistantMessage = async (force = false) => {
    const combined = buildAssistantPayload();

    if (!combined) {
      if (force) {
        assistantMessageId = null;
        lastStreamedAssistantPayload = null;
        resetAssistantBuffers();
      }
      return;
    }

    const id = assistantMessageId ?? (assistantMessageId = randomUUID());
    lastStreamedAssistantPayload = null;

    await persistMessage(
      projectId,
      {
        id,
        role: 'assistant',
        messageType: 'chat',
        content: combined,
        metadata: { cli_type: 'codex' },
      },
      requestId,
      { isStreaming: false, isFinal: true },
    );

    assistantMessageId = null;
    resetAssistantBuffers();
  };

  const emitCommandStart = async (item: Record<string, unknown>) => {
    const id = pickFirstString(item.id) ?? randomUUID();
    const command = pickFirstString(item.command);
    activeCommands.set(id, { command });
    const label = command ? `Running: ${command}` : 'Running command';
    await dispatchToolMessage(
      projectId,
      label,
      {
        toolName: 'Bash',
        tool_name: 'Bash',
        command,
        status: pickFirstString(item.status) ?? 'in_progress',
      },
      requestId,
      { persist: false, isStreaming: true, streamedToolHashes },
    );
  };

  const emitCommandResult = async (item: Record<string, unknown>) => {
    const id = pickFirstString(item.id);
    const tracked = id ? activeCommands.get(id) : undefined;
    if (id) {
      activeCommands.delete(id);
    }
    const command = pickFirstString(item.command) ?? tracked?.command;
    const output = pickFirstString(item.aggregated_output) ?? '';
    const exitCode = typeof item.exit_code === 'number' ? item.exit_code : undefined;
    const status = pickFirstString(item.status);
    const isError = status === 'failed' || (typeof exitCode === 'number' && exitCode !== 0);

    const { content, metadata } = summarizeExecCommand({ command });
    const exitSuffix = typeof exitCode === 'number' ? ` (exit ${exitCode})` : '';
    const messageBody = output.trim();
    const fullContent = messageBody ? `${content}${exitSuffix}\n\n${messageBody}` : `${content}${exitSuffix}`;

    const metaToolNameSnake =
      metadata && typeof (metadata as Record<string, unknown>)['tool_name'] === 'string'
        ? ((metadata as Record<string, unknown>)['tool_name'] as string)
        : undefined;
    const metaToolNameCamel =
      metadata && typeof (metadata as Record<string, unknown>)['toolName'] === 'string'
        ? ((metadata as Record<string, unknown>)['toolName'] as string)
        : undefined;
    const resolvedToolName = metaToolNameCamel ?? metaToolNameSnake ?? 'Bash';

    await dispatchToolMessage(
      projectId,
      fullContent,
      {
        ...metadata,
        toolName: resolvedToolName,
        tool_name: resolvedToolName,
        exitCode,
        status,
        output,
        is_error: isError ? true : undefined,
      },
      requestId,
      { messageType: 'tool_result', streamedToolHashes },
    );
  };

  const emitFileChange = async (item: Record<string, unknown>) => {
    const { content, metadata } = summarizeApplyPatch({
      changes: item.changes as Record<string, unknown> | Array<Record<string, unknown>>,
    });
    const status = pickFirstString(item.status) ?? 'completed';
    const isError = status === 'failed';

    const fileChangeToolNameSnake =
      metadata && typeof (metadata as Record<string, unknown>)['tool_name'] === 'string'
        ? ((metadata as Record<string, unknown>)['tool_name'] as string)
        : undefined;
    const fileChangeToolNameCamel =
      metadata && typeof (metadata as Record<string, unknown>)['toolName'] === 'string'
        ? ((metadata as Record<string, unknown>)['toolName'] as string)
        : undefined;
    const resolvedFileChangeToolName = fileChangeToolNameCamel ?? fileChangeToolNameSnake ?? 'Edit';

    await dispatchToolMessage(
      projectId,
      isError ? `Failed: ${content}` : content,
      {
        ...metadata,
        toolName: resolvedFileChangeToolName,
        tool_name: resolvedFileChangeToolName,
        status,
        is_error: isError ? true : undefined,
      },
      requestId,
      { messageType: 'tool_result', streamedToolHashes },
    );
  };

  const emitTodoListUpdate = async (record: Record<string, unknown>, phase: TodoListPhase) => {
    const rawItems = extractTodoListItems(record);
    const items = normalizeTodoListItems(rawItems);
    const content = buildTodoListContent(items, phase);
    const status =
      pickFirstString(record.status) ??
      (phase === 'completed' ? 'completed' : 'in_progress');
    const metadata = createTodoListMetadata(items, phase, {
      status,
      planId: pickFirstString(record.id),
    });

    await dispatchToolMessage(
      projectId,
      content,
      metadata,
      requestId,
      {
        messageType: phase === 'completed' ? 'tool_result' : 'tool_use',
        persist: phase !== 'update',
        isStreaming: phase === 'update',
        streamedToolHashes,
      },
    );
  };

  const handleItemStarted = async (item: unknown) => {
    if (!item || typeof item !== 'object') {
      return;
    }
    const record = item as Record<string, unknown>;
    const type = pickFirstString(record.type);
    if (!type) {
      return;
    }

    switch (type) {
      case 'command_execution':
        await emitCommandStart(record);
        break;
      case 'todo_list':
        await emitTodoListUpdate(record, 'started');
        break;
      default:
        break;
    }
  };

  const handleItemCompleted = async (item: unknown) => {
    if (!item || typeof item !== 'object') {
      return;
    }
    const record = item as Record<string, unknown>;
    const type = pickFirstString(record.type);
    if (!type) {
      return;
    }

    switch (type) {
      case 'command_execution':
        await emitCommandResult(record);
        break;
      case 'file_change':
        await emitFileChange(record);
        break;
      case 'agent_message': {
        const text = pickFirstString(record.text) ?? '';
        if (text.trim()) {
          agentBuffer = text.trim();
        }
        streamAssistantDraft(true);
        await flushAssistantMessage(true);
        break;
      }
      case 'reasoning': {
        const text = pickFirstString(record.text);
        if (text) {
          thinkingSegments.push(text);
          streamAssistantDraft();
        }
        break;
      }
      case 'todo_list':
        await emitTodoListUpdate(record, 'completed');
        break;
      default: {
        if (record.text && typeof record.text === 'string') {
          thinkingSegments.push(record.text);
          streamAssistantDraft();
        }
        break;
      }
    }
  };

  const handleItemDelta = async (delta: unknown) => {
    if (!delta || typeof delta !== 'object') {
      return;
    }
    const record = delta as Record<string, unknown>;
    const type = pickFirstString(record.type);
    if (type === 'agent_message') {
      const text = pickFirstString(record.text);
      if (text) {
        agentBuffer += text;
        streamAssistantDraft();
      }
    } else if (type === 'reasoning') {
      const text = pickFirstString(record.text);
      if (text) {
        thinkingSegments.push(text);
        streamAssistantDraft();
      }
    } else if (type === 'todo_list') {
      await emitTodoListUpdate(record, 'update');
    }
  };

  child.on('error', (error) => {
    const message = error instanceof Error ? error.message : String(error);
    void (async () => {
      if (hasCompleted) {
        return;
      }
      await flushAssistantMessage(true);
      publishStatus(projectId, 'completed', requestId, 'Codex execution failed to start');
      if (requestId) {
        await markUserRequestAsFailed(requestId, message);
      }
      hasCompleted = true;
    })();
  });

  child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
    if (hasCompleted) {
      return;
    }
    const detailParts: string[] = [];
    if (typeof code === 'number') {
      detailParts.push(`exit code ${code}`);
    }
    if (signal) {
      detailParts.push(`signal ${signal}`);
    }
    const detail = detailParts.length > 0 ? detailParts.join(', ') : 'unexpected shutdown';
    void (async () => {
      await flushAssistantMessage(true);
      publishStatus(projectId, 'completed', requestId, 'Codex session ended unexpectedly');
      if (requestId) {
        await markUserRequestAsFailed(requestId, `Codex process terminated (${detail})`);
      }
      hasCompleted = true;
    })();
  });

  try {
    publishStatus(projectId, 'running', requestId);

    for await (const line of rl) {
      if (!line.trim()) continue;
      let event: Record<string, unknown>;
      try {
        event = JSON.parse(line) as Record<string, unknown>;
      } catch (error) {
        console.warn('[CodexService] Failed to parse Codex event:', line);
        continue;
      }

      const eventType = pickFirstString(event.type);
      switch (eventType) {
        case 'item.started':
          await handleItemStarted((event as { item?: unknown }).item ?? null);
          break;
        case 'item.delta':
          await handleItemDelta((event as { delta?: unknown }).delta ?? null);
          break;
        case 'item.completed':
          await handleItemCompleted((event as { item?: unknown }).item ?? null);
          break;
        case 'item.failed': {
          const item = (event as { item?: unknown }).item ?? null;
          await handleItemCompleted(item);
          const message =
            pickFirstString((item as Record<string, unknown> | null)?.error) ??
            'Codex execution failed';
          await flushAssistantMessage(true);
          publishStatus(projectId, 'completed', requestId, message);
          if (requestId) {
            await markUserRequestAsFailed(requestId, message);
          }
          hasCompleted = true;
          return;
        }
        case 'error': {
          const message =
            pickFirstString((event as { error?: unknown }).error) ??
            pickFirstString((event as { message?: string }).message) ??
            (stderrBuffer.slice(-5).join('\n') || 'Codex execution failed');
          await flushAssistantMessage(true);
          publishStatus(projectId, 'completed', requestId, 'Codex execution ended with errors');
          if (requestId) {
            await markUserRequestAsFailed(requestId, message);
          }
          hasCompleted = true;
          return;
        }
        case 'turn.completed':
          hasCompleted = true;
          break;
        default:
          if (process.env.NODE_ENV !== 'production') {
            console.debug('[CodexService] Unhandled Codex event:', event);
          }
          break;
      }
    }

    await flushAssistantMessage(true);
    hasCompleted = true;

    publishStatus(projectId, 'completed', requestId);
    if (requestId) {
      await markUserRequestAsCompleted(requestId);
    }
  } catch (error) {
    await flushAssistantMessage(true);
    const message =
      error instanceof Error ? error.message : stderrBuffer.slice(-5).join('\n') || 'Codex execution failed';
    publishStatus(projectId, 'completed', requestId, 'Codex execution terminated');
    if (requestId) {
      await markUserRequestAsFailed(requestId, message);
    }
    throw error;
  } finally {
    rl.close();
    if (!child.killed) {
      child.stdin?.end();
      child.kill();
    }
  }
}

export async function initializeNextJsProject(
  projectId: string,
  projectPath: string,
  initialPrompt: string,
  model: string = CODEX_DEFAULT_MODEL,
  requestId?: string,
): Promise<void> {
  const fullPrompt = `
Create a new Next.js 15 application with the following requirements:
${initialPrompt}

Use App Router, TypeScript, and Tailwind CSS.
Set up the basic project structure and implement the requested features.
`.trim();

  await executeCodex(projectId, projectPath, fullPrompt, model ?? getDefaultModelForCli('codex'), requestId, true);
}

export async function applyChanges(
  projectId: string,
  projectPath: string,
  instruction: string,
  model: string = CODEX_DEFAULT_MODEL,
  _sessionId?: string,
  requestId?: string,
): Promise<void> {
  await executeCodex(projectId, projectPath, instruction, model ?? getDefaultModelForCli('codex'), requestId, false);
}
